package config;

import com.support.StringToLocalDateConverter;
import lombok.Data;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.RestClients;
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;
import org.springframework.data.elasticsearch.config.ElasticsearchConfigurationSupport;
import org.springframework.data.elasticsearch.core.AbstractElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchConverter;
import org.springframework.data.elasticsearch.core.convert.ElasticsearchCustomConversions;
import org.springframework.data.elasticsearch.core.convert.MappingElasticsearchConverter;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 与SpringDataEsRepositoryConfig配置一样，但使用了spring提供的一些父配置类与自定义的转换器，
 * 继承父类的原因，这里没有配置ElasticsearchRestTemplate bean的注册
 */
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.repository.dao")
@ComponentScan("com.repository")
public class SpringDataEsRepositoryConfig2 extends AbstractElasticsearchConfiguration {

    @Override
    public RestHighLevelClient elasticsearchClient() {
        return RestClients.create(ClientConfiguration.create("localhost:9200"))
                .rest();
    }

    //region 自定义转换器的编写与注册

    /**
     * <h3>类型说明及依赖关系</h3>
     * <ul>
     *     <li>{@link ElasticsearchRestTemplate}需要{@link RestHighLevelClient}、{@link ElasticsearchConverter}</li>
     *     <li>{@link MappingElasticsearchConverter}是{@link ElasticsearchConverter}的实现类，其创建通常需要干如下事情
     *      <ul>
     *          <li>创建{@link SimpleElasticsearchMappingContext}对象
     *              <ul>
     *                  <li>{@link SimpleElasticsearchMappingContext}通常可以直接new出来使用即可，
     *                  见{@link AbstractElasticsearchTemplate#createElasticsearchConverter()} </li>
     *                  <li>但{@link ElasticsearchConfigurationSupport#elasticsearchMappingContext(ElasticsearchCustomConversions)}
     *                  这个bean方法对其创建做了一些额外的事情，尽量利用上这个bean方法里的逻辑而
     *                  不要直接new{@link SimpleElasticsearchMappingContext}</li>
     *              </ul>
     *          </li>
     *          <li>调用{@link MappingElasticsearchConverter#setConversions(CustomConversions)}
     *              <ul>
     *                  <li>{@link MappingElasticsearchConverter#setConversions(CustomConversions)}方法
     *                  需要的参数类型是CustomConversions</li>
     *                  <li>{@link CustomConversions}的实现类是{@link ElasticsearchCustomConversions}</li>
     *                  <li>{@link ElasticsearchCustomConversions}实例化时在其构造函数里可以传递自定义的转换器</li>
     *              </ul>
     *          </li>
     *          <li>调用{@link MappingElasticsearchConverter#afterPropertiesSet()} ，
     *          这一步把上一步的自定义转换器添加到MappingElasticsearchConverter内的转换服务里</li>
     *      </ul>
     *     </li>
     * </ul>
     * <h3>spring data es版本4.3.0自定义转换器执行逻辑</h3>
     * 配置类继承于AbstractElasticsearchConfiguration时，整个ElasticsearchRestTemplate创建逻辑如下
     * <ul>
     *     <li>{@link #elasticsearchOperations(ElasticsearchConverter, RestHighLevelClient)} bean方法获取转换器与客户端</li>
     *     <li>不自己注册ElasticsearchConverter类型的bean的话，
     *     父类有一个bean方法{@link #elasticsearchEntityMapper(SimpleElasticsearchMappingContext, ElasticsearchCustomConversions)}是用来创建转换器</li>
     *     <li>第二步的bean需要ElasticsearchCustomConversions类型的bean</li>
     *     <li>你在配置类中编写了自定义转换的bean方法 elasticsearchCustomConversions，这个bean就被第二步使用到了</li>
     *     <li>但是：elasticsearchEntityMapper bean方法内创建MappingElasticsearchConverter时，调用了setConversions，
     *     没有调用afterPropertiesSet,这样会导致自定义的转换器没有添加到MappingElasticsearchConverter使用的GenericConversionService里，
     *     导致自定义的转换器失效，运行时会出现找不到对应转换器的异常</li>
     * </ul>
     * <h3>bug原因</h3>
     * <p>出现这个bug的主要原因是MappingElasticsearchConverter是直接new 出来，不走spring 生命周期流程，
     * 所以afterPropertiesSet不会自动被调用，需要手动调用。在{@link ElasticsearchRestTemplate}的构造函数中，
     * 其中一个构造函数调用了createElasticsearchConverter()方法，此方法告诉你如何创建ElasticsearchConverter以及
     * 要调用afterPropertiesSet().而其另外一个构造函数是传递进来了一个ElasticsearchConverter，所以这个传递进来的
     * ElasticsearchConverter对象如果没有调用afterPropertiesSet方法会导致两者不一致，这也是bug的源头所在</p>
     * <h3>bug解决办法</h3>
     * <ul>
     *     <li>添加一个模仿父类的{@link ElasticsearchConfigurationSupport#elasticsearchEntityMapper(SimpleElasticsearchMappingContext, ElasticsearchCustomConversions)}
     *     这个bean方法的写法（复制即可），此方法就是用来创建ElasticsearchConverter对象的，
     *     在此bean方法里添加上对afterPropertiesSet方法的调用即可</li>
     *     <li>或者重写elasticsearchOperations这个bean方法，创建ElasticsearchRestTemplate时采用只有一个参数RestHighLevelClient的构造函数，
     *     但这种方法更不好，因为其创建ElasticsearchConverter时压根就没有调用setConversions，
     *     并且对SimpleElasticsearchMappingContext的处理也比较简单，所以这种解决办法不推荐</li>
     * </ul>
     * @return
     */
    @Bean
    @Override
    public ElasticsearchCustomConversions elasticsearchCustomConversions() {
       // return new ElasticsearchCustomConversions(Arrays.asList(new AddressToMap(), new MapToAddress()));
        return new ElasticsearchCustomConversions(Arrays.asList(StringToLocalDateConverter.INSTANCE));
    }

    @Override
    public ElasticsearchConverter elasticsearchEntityMapper(SimpleElasticsearchMappingContext elasticsearchMappingContext, ElasticsearchCustomConversions elasticsearchCustomConversions) {
        MappingElasticsearchConverter elasticsearchConverter = new MappingElasticsearchConverter(
                elasticsearchMappingContext);
        elasticsearchConverter.setConversions(elasticsearchCustomConversions);
        //加上这一行代码
        elasticsearchConverter.afterPropertiesSet();
        return elasticsearchConverter;
    }

    //endregion

    // region
    // Converter:org.springframework.core.convert.converter.Converter 别导错了类
    @WritingConverter
    static class AddressToMap implements Converter<Address, Map<String, Object>> {


        @Override
        public Map<String, Object> convert(Address source) {

            LinkedHashMap<String, Object> target = new LinkedHashMap<>();
            target.put("ciudad", source.getCity());
            // ...

            return target;
        }
    }

    @ReadingConverter
    static class MapToAddress implements Converter<Map<String, Object>, Address> {

        @Override
        public Address convert(Map<String, Object> source) {
            Address address = null;
            // ...
            return address;
        }
    }
    @Data
    static  class Address{
        private String city;
    }
    // endregion
}
